package com.hyw.encryption.service.impl;

import java.time.Duration;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;

import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

import com.hyw.encryption.keystore.dto.UserKeyStoreDTO;
import com.hyw.encryption.service.EncryptionKeys;
import com.hyw.encryption.service.IUserKeyStoreService;
import com.hyw.encryption.utils.JsonUtils;
import com.hyw.encryption.utils.RSAUtils;
import com.hyw.encryption.utils.RandomStringUtils;
import com.hyw.encryption.utils.RandomStringUtils.RandomType;

/**
 * 对于游客账户的清洗，目前不建议由当前框架维护。应该提供接口，由业务层统一调整清洗，当然理论上，也可以不清除。
 * 
 * @author Hongyu
 */
public class DefaultUserKeyStoreServiceImpl implements IUserKeyStoreService {

  // 游客账户的有效期至少保持大于1小时
  private static final long MIN_ANONYMOUS_EXPIRED_IN_MILLIS = Duration.ofHours(1).toMillis();

  private final RedisTemplate<String, String> redisTemplate;

  private final long anonymousTimeToLiveInMillis;

  private final Predicate<String> annoyPredicat;

  public DefaultUserKeyStoreServiceImpl(RedisTemplate<String, String> redisTemplate, long anonymousTimeToLiveInSeconds,
      Predicate<String> annoyPredicat) {
    super();
    Objects.requireNonNull(redisTemplate, "redisTemplate不能为空");
    Objects.requireNonNull(annoyPredicat, "annoyPredicat不能为空");

    // 时间单位转换，并检查最小TTL的有效性
    this.anonymousTimeToLiveInMillis = TimeUnit.MILLISECONDS.convert(anonymousTimeToLiveInSeconds, TimeUnit.SECONDS);
    Assert.isTrue(this.anonymousTimeToLiveInMillis >= MIN_ANONYMOUS_EXPIRED_IN_MILLIS, "游客账户的有效期至少保持大于1小时");

    this.redisTemplate = redisTemplate;
    this.annoyPredicat = annoyPredicat;
  }

  @Override
  public void save(UserKeyStoreDTO keystore) {
    validate(keystore);
    if (this.isAnonymous(keystore.getUserId())) {
      // 对匿名用户做添加时的时间维度跟踪，以便于定时做清洗游客信息
      redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
        // ts需要做时间累计
        connection.zAdd(EncryptionKeys.ANONYMOUS_EXPIRED_KEY.getBytes(),
            keystore.getTimestamp() + anonymousTimeToLiveInMillis, keystore.getUserId().getBytes());

        connection.hSet(EncryptionKeys.ANONYMOUS_STORE_KEY.getBytes(), keystore.getUserId().getBytes(),
            JsonUtils.toJsonBytes(keystore));

        return null;
      });
    } else {
      redisTemplate.<String, String> opsForHash().put(EncryptionKeys.REG_STORE_KEY, keystore.getUserId(),
          JsonUtils.toJsonString(keystore));
    }
  }

  @Override
  public Optional<UserKeyStoreDTO> load(String userId) {
    String key = this.isAnonymous(userId) ? EncryptionKeys.ANONYMOUS_STORE_KEY : EncryptionKeys.REG_STORE_KEY;
    return Optional.ofNullable(redisTemplate.<String, String> opsForHash().get(key, userId))
        .map(json -> JsonUtils.parseJson(json, UserKeyStoreDTO.class));
  }

  @Override
  public String[] generateKeyPair(String uid) {
    return RSAUtils.generate();
  }

  @Override
  public String generateAESKey(String uid) {
    return RandomStringUtils.random(RandomType.MIXED, 16);
  }

  @Override
  public boolean isAnonymous(String userId) {
    return this.annoyPredicat.test(userId);
  }

  @Override
  public boolean remove(String userId) {
    boolean removed = false;
    if (this.isAnonymous(userId)) {
      removed = redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
        connection.zRem(EncryptionKeys.ANONYMOUS_EXPIRED_KEY.getBytes(), userId.getBytes());
        connection.hDel(EncryptionKeys.ANONYMOUS_STORE_KEY.getBytes(), userId.getBytes());
        return null;
      }).stream().anyMatch(obj -> Objects.nonNull(obj));
    } else {
      removed = redisTemplate.<String, String> opsForHash().delete(EncryptionKeys.REG_STORE_KEY, userId) > 0;
    }
    return removed;
  }

  private void validate(UserKeyStoreDTO keystore) {
    Assert.isTrue(StringUtils.hasText(keystore.getUserId()), "用户ID不允许为空");
    Assert.isTrue(StringUtils.hasText(keystore.getCk()), "Client Public Key 不允许为空");
    Assert.isTrue(StringUtils.hasText(keystore.getSk()), "Server Private Key 不允许为空");
    Assert.isTrue(StringUtils.hasText(keystore.getAk()), "Server AES Key 不允许为空");
  }

}
